<?php
namespace WDB;
use SQLDriver,
    WDB\Exception;

/**
 * Single database connection (database driver encapsulation class).
 * @property-read string $name connection name identifier
 * @property-read \WDB\Structure\ConnectionConfig $connectionConfig connection configuration
 * @property-read \WDB\SQLDriver\iSQLDriver $driver sql database driver
 * @property-read Analyzer\Schema $schema current active schema
 *
 * @author Richard Ejem <richard(at)ejem.cz>
 * @package WDB
 */
final class Database extends BaseObject
{
    /** @var iSQLDriver $driver*/
    private $driver;

    /** @var \WDB\Structure\ConnectionConfig[] */
    private static $registeredConnections = array();

    /** @var \WDB\Structure\ConnectionConfig */
    private $connectionConfig;

    /** @var boolean $connected */
    private $connected = FALSE;

    /** @var Database[] $connections */
    private static $connections;

    /**@var Analyzer\SchemaCache */
    private $schemaCache;

    /** @var string $currentSchema current schema name */
    private $currentSchema;

    /** @var string $name wdb connection persistent name*/
    private $name;

    /** @var Query\Log */
    private $log;

    /** @var int */
    private $transactionLevel;

    /** @var bool */
    private $mustRollback = FALSE;

    const BU_REPLACE = 1;
    const BU_EXCEPTION = 2;

    public static $brokenUTFMode = self::BU_REPLACE;

    public static function validateBrokenUTF($value)
    {
        if (!mb_check_encoding($value, 'utf8'))
        {
            switch (self::$brokenUTFMode)
            {
                case self::BU_EXCEPTION:
                    throw new Exception\InvalidUTFString($value);
                case self::BU_REPLACE:
                    $value = preg_replace('~[^(\x00-\x7F)]~','?', $value);
                    break;
                default:
                    throw new Exception\InvalidBrokenUTFSetting($value);
            }
        }
        return $value;
    }

    public function getLog()
    {
        return $this->log;
    }

    /**
     *
     * @param string|Structure\ConnectionConfig name of connection or connection configuration
     * @throws Exception\ConfigKeyMissing when a named connection is not found
     */
    private function __construct($connection)
    {
        $cname = null;
        // load configuration of named connection from a registered connection or global config
        if (!$connection instanceof Structure\ConnectionConfig)
        {
            if (isset(self::$registeredConnections[$connection]))
            {
                $conn = self::$registeredConnections[$connection];
            }
            elseif (Config::exists("connections.$connection"))
            {
                $conn = Config::read("connections.$connection");
                if (!isset($conn['user'])) $conn['user'] = NULL;
                if (!isset($conn['password'])) $conn['password'] = NULL;
                if (!isset($conn['schema'])) $conn['schema'] = NULL;
                if (!isset($conn['charset'])) $conn['charset'] = NULL;
                if (!isset($conn['port'])) $conn['port'] = NULL;

                $conn = new Structure\ConnectionConfig($conn);
            }
            else
            {
                throw new Exception\ConfigKeyMissing("connection with name '$connection' not found");
            }
            $cname = $connection;
        }
        else
        {
            if (isset($conn['name']))
            {
                $cname = $conn['name'];
            }
        }

        //verify configuration
        if (!isset($conn->driver) || !isset($conn->host))
            throw new Exception\ConfigInsufficient("driver and host are required fields for database connection.");

        //set default name if no name set
        if ($cname === NULL)
        {
            $cname = 'NN-'.$conn->host.'-'.$conn->user.'-'.$conn->schema.'-'.$conn->port;
        }

        //postfix name number if connection name already exists
        if (isset(self::$connections[$cname]))
        {
            for ($i = 0; isset(self::$conns[$cname.$i]); ++$i){}
            $cname .= $i;
        }

        //initialize connection
        $driver_class = '\\WDB\\SQLDriver\\'.$conn->driver;
        if (!class_exists($driver_class))
        {
            throw new Exception\DriverNotFound("driver '{$conn->driver}' not found");
        }

        $this->driver = new $driver_class();
        $this->connectionConfig = $conn;
        $this->schemaCache = new Analyzer\SchemaCache($this);
        $this->currentSchema = $this->connectionConfig->schema;
        self::$connections[$cname] = $this;
        $this->name = $cname;
        $this->log = new Query\Log();
        if (isset($conn->loglevel))
        {
            $this->log->level = $conn->loglevel;
        }
        $this->transactionLevel = 0;
    }

    /**
     * Registers a database configuration under specified name for the current execution.
     *
     * @param string $name identifier
     * @param WDB\Structure\ConnectionConfig $config configuration
     *
     * @throws Exception\DuplicateName
     */
    public static function registerConnection($name, $config)
    {
        //TODO
        if (isset(self::$registeredConnections[$name]))
        {
            throw new Exception\DuplicateName("Connection with name $name already exists");
        }
        self::$registeredConnections[$name] = $config;
    }

    /**
     * Unregisters a database configuration.
     *
     * @param string identifier
     */
    public static function unregisterConnection($name)
    {
        if (isset(self::$registeredConnections[$name])) unset (self::$registeredConnections[$name]);
    }

    /**
     * Singleton container for database connection.
     * returns instance of \WDB\Database connected with a database
     * (NULL returns connection named 'default' in WDB configuration)
     *
     * @param string database connection identifier
     * @return WDB\Database
     */
    public static function getInstance($name)
    {
        if (!isset(self::$connections[$name]))
        {
            self::$connections[$name] = new self($name);
        }
        return self::$connections[$name];
    }

    public static function getDefault()
    {
        return self::getInstance('default');
    }

    /**
     * connection name identifier
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * database driver
     *
     * @return \WDB\SQLDriver\iSQLDriver
     */
    public function getDriver()
    {
        $this->lazyConnect();
        return $this->driver;
    }

    /**
     * checks if this object is connected to database and connects if it is not
     */
    protected function lazyConnect()
    {
        if (!$this->connected)
        {
            $this->connect();
            $this->connected = TRUE;
        }
    }

    /**
     * connects to the database. this is not needed to call because database will
     * connect automatically with the first query, only if you want to intentionally create connection at some point.
     */
    public function connect()
    {
        $this->driver->connect($this->connectionConfig);
    }

    /**
     * disconnect from the database. Not needed to, PHP garbage collector will do the job
     */
    public function disconnect()
    {
        $this->driver->disconnect();
        $this->connected = FALSE;
    }

    /**
     * performs query on a database.
     *
     * @param Query\Query $query
     * @return Result\iQueryResult
     */
    public function query(Query\Query $query)
    {
        $this->lazyConnect();
        $query->database = $this;
        try
        {
            //create clone query which has bound this database. it allows the query result to later manipulate
            //and re-execute the query.
            $query = clone $query;
            $query->setDatabase($this);

            $result = $this->driver->query($query);
            $this->log->log($result);
            return $result;
        }
        catch (Exception\QueryError $ex)
        {
            $this->log->logError($query, $ex);
            throw $ex;
        }
    }

    /**
     * performs query on a database.
     *
     * @param string
     * @param bool if true, multiple queries are allowed in one call
     * @return mixed
     */
    public function queryRaw($queryStr, $multi = FALSE)
    {
        $this->lazyConnect();

        $query = new Query\Raw($queryStr);
        $query->database = $this;

        try
        {
            $result = $this->driver->queryRaw($queryStr, $multi);
            $this->log->log($result);
            return $result;
        }
        catch (Exception\QueryError $ex)
        {
            $this->log->logError($query, $ex);
            throw $ex;
        }
    }

    /**
     * returns list of tables in currently selected schema
     *
     * @return Analyzer\Table[]
     */
    public function getTables()
    {
        return $this->schemaCache->getTables();
    }


    /**
     * get connection configuration information
     * @return Structure\ConnectionConfig
     */
    public function getConnectionConfig()
    {
        return $this->connectionConfig;
    }

    /**
     * Switches connection to another table schema.
     *
     * @param string|Analyzer\Schema schema to switch
     */
    public function setSchema($schema)
    {
        if ($schema instanceof Analyzer\Schema)
            $schema = $schema->name;
        $this->driver->changeSchema($schema);
        $this->currentSchema = $schema;
    }

    /**
     * returns a schema in this database
     *
     * @param string|NULL schema identifier - null for current
     * @return WDB\Analyzer\iSchema
     * @throws WDB\Exception\SchemaNotFound
     */
    public function getSchema($name = NULL)
    {
        if ($name === NULL)
        {
            return $this->schemaCache->getSchema($this->currentSchema);
        }
        else
        {
            return $this->schemaCache->getSchema($name);
        }
    }

    /**
     *
     * @param Query\Element\TableIdentifier table identifier
     * @return WDB\Analyzer\iTable
     * @throws WDB\Exception\SchemaNotFound
     * @throws WDB\Exception\TableNotFound
     */
    public function getTable(Query\Element\TableIdentifier $identifier)
    {
        $tables = $this->getSchema($identifier->schema)->getTables();
        if (!isset($tables[$identifier->table])) throw new Exception\TableNotFound ("Table {$identifier->schema}.{$identifier->table} not found");
        return $tables[$identifier->table];
    }

    public function startTransaction()
    {
        if (++$this->transactionLevel == 1)
        {
            $this->getDriver()->startTransaction();
        }
    }

    public function commit()
    {
        if ($this->transactionLevel == 0)
        {
            throw new Exception\InvalidOperation("No running transaction to commit");
        }
        if (--$this->transactionLevel == 0)
        {
            if ($this->mustRollback) throw new Exception\InvalidOperation("Insidious transaction requested rollback, unable to commit");
            $this->getDriver()->commit();
        }
    }

    public function commitable()
    {
        return $this->transactionLevel > 0 && !$this->mustRollback;
    }

    public function rollback()
    {
        if ($this->transactionLevel == 0)
        {
            throw new Exception\InvalidOperation("No running transaction to rollback");
        }
        if (--$this->transactionLevel == 0)
        {
            $this->getDriver()->rollback();
            $this->mustRollback = FALSE;
        }
        else
        {
            $this->mustRollback = TRUE;
        }
    }
}
